////////////////////////////////////////////////////////////////////////////////
//
//  FLEX USER GROUP HOKURIKU
//  Copyright 2009 Flex User Group Hokuriku
//  All Rights Reserved.
// 
//  NOTICE: Flex User Group Hokuriku permits you to use, 
//  modify, and distribute this file 
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package net.fxug.hokuriku.chocolat.service
{
    import com.adobe.utils.DateUtil;
    import com.swfjunkie.tweetr.Tweetr;
    import com.swfjunkie.tweetr.data.objects.SearchResultData;
    import com.swfjunkie.tweetr.events.TweetEvent;

    import flash.errors.IllegalOperationError;
    import flash.events.TimerEvent;
    import flash.utils.Dictionary;
    import flash.utils.Timer;
    import flash.utils.getQualifiedClassName;

    import mx.logging.ILogger;

    import net.fxug.hokuriku.chocolat.events.VoteEvent;
    import net.fxug.hokuriku.chocolat.events.VoteStatusEvent;
    import net.fxug.hokuriku.chocolat.model.VoteData;
    import net.fxug.hokuriku.chocolat.model.VoteInfo;
    import net.fxug.hokuriku.chocolat.utils.LoggingUtil;

    /**
     * Twitter検索を用いた投票機能管理クラス。
     * search.twitter.com/に定期的にリクエストを投げた検索結果を分析して、投票イベントを呼び出す。
     *
     * Twitterの取得APIに取得制限150/hがあるため、再帰的に取得するような方法をとると問題が発生する可能性があるので、
     * 再帰的に取得する方法を取っておらず、以前の取得した検索結果から１回で取得できる最大数の100件のみ取得している。
     *
     * 検索実行頻度は、TweetSearchSpanResolver#findWatingTime()のアルゴリズムに委ねられる。
     *
     * @see net.fxug.hokuriku.chocolat.service.TweetSearchSpanResolver#findWatingTime
     * @author hikaruworld
     */
    public class TwitterSearchVoteWatcher implements IVoteWatcher
    {
        //-------------------------------------------------------------------------
        //
        // クラス定数
        //
        //-------------------------------------------------------------------------
        /** 自身を表すインターフィエスプロトコル名 */
        public static const WATCHER_TYPE:String = "twitter";
        /** へぇ(+)を示すtweetのタグメッセージ */
        private static const GOOD_TWEET:String = "#good";
        /** ぶぅ(-)を示すtweetのタグメッセージ */
        private static const BAD_TWEET:String = "#bad";

        /** 半角スペースをURIエンコードした値 */
        private static const ENCODED_SPACE:String = encodeURIComponent(" ");

        /** ロガー */
        private static var logger:ILogger = LoggingUtil.getLogger(TwitterSearchVoteWatcher);
        /**
         * ユーザ別のつぶやき回数を保持するDictionary
         */
        private var votedMap:Dictionary;
        /** 監視に利用されるタイマーのインスタンス */
        private var watchTimer:Timer;
        /**
         * Twitter検索時にキーとなる文字列
         * 複数検索時は半角スペースで区切る。
         */
        private var searchString:String;
        /** 監視実行間隔 */
        private var ms:uint;
        /** Twitter検索APIライブラリのインスタンス */
        private var tweetr:Tweetr;
        /**
         * 基点ID
         * このIDを基準に以降のtweetsを取得する。
         */
        private var sinceId:uint;
        /** 検索期間取得間隔用 */
        private var resolver:TweetSearchSpanResolver
        /** 以前に発行された成功イベントor失敗イベント */
        private var dispatchedEventName:String;

        private var startupTime:Date;

        //-------------------------------------------------------------------------
        //
        // プロパティ(getter/setter)
        //
        //-------------------------------------------------------------------------
        /**
         * 監視停止フラグ
         * この値がtrueの場合に監視処理を停止する。
         */
        private var _stopFlag:Boolean = false;

        /**
         * コンストラクタ
         * @param hashTag 検索文字列(twitterのハッシュタグ)
         * @param sessionBaseTime セッション時間(ミリ秒)
         * @param sessionTag 検索文字列(セッションタグ) --- Optional
         */
        public function TwitterSearchVoteWatcher(hashTag:String, sessionBaseTime:int, sessionTag:String = null)
        {
            // 引数を検証
            if (hashTag == null || hashTag.length == 0)
            {
                throw new ArgumentError("検索条件[hashTag]が設定されていません");
            }
            if (sessionBaseTime < 1000 * 60)
            {
                throw new ArgumentError("設定されたsessionBaseTime[" + sessionBaseTime + "]が小さすぎます。設定できる最小のセッション時間は1分(60000ms)です");
            }
            this.searchString = hashTag + " " + sessionTag;
            if (this.getAddedGoodBadKeyword(this.searchString).length > 140)
            {
                throw new IllegalOperationError("検索文字列::" + encodeURIComponent(hashTag) + "::はUTF-8でURLエンコードされた状態で140文字以内である必要があります。");
            }

            this.resolver = new TweetSearchSpanResolver(sessionBaseTime);

            this.dispatchedEventName = null;
            this.ms = TweetSearchSpanResolver.DEFAULT_DELAY;

            this.votedMap = new Dictionary();
            this.startupTime = new Date();

        }

        //-------------------------------------------------------------------------
        //
        // メソッド
        //
        //-------------------------------------------------------------------------
        /**
         * @inheritDoc net.fxug.hokuriku.chocolat.service.IVoteWatcher#getInterfaceName()
         */
        public function getInterfaceName():String
        {
            return WATCHER_TYPE;
        }

        /**
         * @inheritDoc net.fxug.hokuriku.chocolat.service.IVoteWatcher#start()
         */
        public function start():void
        {
            this.watchTimer = new Timer(this.ms, 0);
            this.watchTimer.addEventListener(TimerEvent.TIMER, timerHandler);
//            this.watchTimer.start();

            VoteManager.getInstance().addEventListener(VoteStatusEvent.READY_EVENT, readyEventInitHandler);

            VoteManager.getInstance().onReady(this);
        }

        private function readyEventInitHandler(event:VoteStatusEvent):void
        {
            VoteManager.getInstance().removeEventListener(VoteStatusEvent.READY_EVENT, readyEventInitHandler);

            this.tweetr = new Tweetr();
            this.tweetr.addEventListener(TweetEvent.COMPLETE, tweetCompleteHandler);
            this.tweetr.addEventListener(TweetEvent.COMPLETE, tweetrTimerStartHandler);
            this.tweetr.addEventListener(TweetEvent.FAILED, tweetFailedHandler);
            this.tweetr.addEventListener(TweetEvent.STATUS, tweetStatusHandler);

            this.tweetr.search(this.getAddedGoodBadKeyword(this.searchString), null, 100, 1);
        }

        private function tweetrTimerStartHandler(tweetEvent:TweetEvent):void
        {
            this.tweetr.removeEventListener(TweetEvent.COMPLETE, tweetrTimerStartHandler);
            this.watchTimer.start();
        }

        /**
         * @inheritDoc net.fxug.hokuriku.chocolat.service.IVoteWatcher#isEnabled()
         */
        public function isEnabled():Boolean
        {
            return this.watchTimer == null ? false : this.watchTimer.running;
        }

        /**
         * @inheritDoc net.fxug.hokuriku.chocolat.service.IVoteWatcher#destroy()
         */
        public function destroy():void
        {
            this._stopFlag = true;
            if (this.watchTimer != null && this.watchTimer.running)
            {
                this.watchTimer.stop();
                this.watchTimer.removeEventListener(TimerEvent.TIMER, timerHandler);
            }
            if (this.tweetr != null)
            {
                try
                {
                    this.tweetr.destroy();
                    this.tweetr.removeEventListener(TweetEvent.COMPLETE, tweetCompleteHandler);
                    this.tweetr.removeEventListener(TweetEvent.STATUS, tweetStatusHandler);
                    this.tweetr.removeEventListener(TweetEvent.FAILED, tweetFailedHandler);
                }
                catch (e:Error)
                {
                    // FIXME Tweetrのライブラリを直せないため、try-catchで暫定的に重複closeを回避する。
                    logger.warn(e.getStackTrace());
                }
            }
        }

        public function toString():String
        {
            return getQualifiedClassName(TwitterSearchVoteWatcher) + ":" + WATCHER_TYPE;
        }

        /**
         * Good/BadプロトコルのOR検索を付与した検索文字列を返す。
         * デフォルトでURIエンコードした文字列が返される。
         */
        private function getAddedGoodBadKeyword(string:String = null):String
        {
            var added:String;
            if (string == null)
            {
                added = encodeURIComponent(this.searchString);
            }
            else
            {
                added = encodeURIComponent(string);
            }
            var ret:String = added + ENCODED_SPACE + encodeURIComponent(TwitterSearchVoteWatcher.GOOD_TWEET) + ENCODED_SPACE + "OR" + ENCODED_SPACE + encodeURIComponent(TwitterSearchVoteWatcher.BAD_TWEET);
            logger.info("Search Query parameter is {0}", ret);
            return ret;
        }

        //-------------------------------------------------------------------------
        //
        // イベントハンドラ
        //
        //-------------------------------------------------------------------------
        /**
         * タイマーカウント時のイベントハンドラ
         * @param timerEvent	Timerイベント
         */
        private function timerHandler(timerEvent:TimerEvent):void
        {
            if (this._stopFlag)
            {
                this.watchTimer.stop();
                return;
            }
            logger.debug("Twitter Search count [{0}]", this.watchTimer.currentCount);
            this.tweetr.search(this.getAddedGoodBadKeyword(this.searchString), null, 100, 1, this.sinceId);
        }

        /**
         * 検索処理完了後に実行されるイベントハンドラ
         * TODO 200なのにも関わらず、エラーになるケースがある。
         * @param tweetEvent つぶやきイベント
         */
        private function tweetCompleteHandler(tweetEvent:TweetEvent):void
        {
            logger.debug("tweetCompleteHandler info:{0},", tweetEvent.info);
            // 無効化している場合のみ有効化してイベントをdispatch
            if (this.dispatchedEventName == null || this.dispatchedEventName == VoteStatusEvent.INACTIVE_EVENT)
            {
                this.dispatchedEventName = VoteStatusEvent.ACTIVE_EVENT;
                var statusEvent:VoteStatusEvent = new VoteStatusEvent(VoteStatusEvent.ACTIVE_EVENT, null);
                VoteManager.getInstance().dispatchStatus(statusEvent);
            }
            // 検索時間を再設定
            // TODO これでいいのか検証すること。...
            this.watchTimer.delay = this.resolver.findWatingTime();

            // 検索結果が見つからなかった場合は処理しない。
            var resultAry:Array = tweetEvent.responseArray as Array;
            if (resultAry == null || resultAry.length == 0)
            {
                logger.info("Twitter検索を行いましたが、何も検索で見つかりませんでした");
                return;
            }
            // 取得した最新データからsinceIdを取得しておく。

            var goodArray:Array = [];
            var badArray:Array = [];

            var index:int = resultAry.length;
            for (var i:int = 0; i < index; i++)
            {
                var rd:SearchResultData = resultAry[i] as SearchResultData;
                // FIXME 暫定対応。since_Idを見て以前のものを処理しない
                // 本来であればsince_idを指定した場合に発行去
                if (rd.id <= this.sinceId)
                {
//                    trace("このIDは既に投票されているため処理を行いません。[resultData::beforeId]=[" + rd.id + "::" + this.since_id + "]");
                    continue;
                }
                // 日付が開始時間より前の投票は無視する
                // XXX この検証ロジックをコメントアウトすればすべての取得結果を都度反映する。
                var rdCreated:Date = DateUtil.parseW3CDTF(rd.createdAt);
                if (DateUtil.compareDates(rdCreated, this.startupTime) == 1)
                {
                    continue;
                }
                var twet:VoteData = VoteData.convertFromSearchResultData(rd);

                var votedInfo:VoteInfo = this.votedMap[twet.id];
                if (!votedInfo)
                {
                    votedInfo = new VoteInfo(twet.id);
                }

                // GOOD優先で処理
                if (twet.text.indexOf(GOOD_TWEET) > -1)
                {
                    var goodBool:Boolean = votedInfo.addAndCheck(VoteEvent.GOOD_EVENT);
                    twet.voteEnabled = goodBool;
                    goodArray.push(twet);
                }
                else if (twet.text.indexOf(BAD_TWEET) > -1)
                {
                    var badBool:Boolean = votedInfo.addAndCheck(VoteEvent.BAD_EVENT);
                    twet.voteEnabled = badBool;
                    badArray.push(twet);
                }
                this.votedMap[twet.id] = votedInfo;
            }
            var resultData:SearchResultData = resultAry[0] as SearchResultData;
            logger.debug("SearchResultData#id::{0} > since_id::{1}", resultData.id, this.sinceId);

            this.sinceId = resultData.id;

            if (goodArray.length > 0)
            {
                logger.debug("Good protocol count up {0}", goodArray.length);
                var hee:VoteEvent = new VoteEvent(VoteEvent.GOOD_EVENT, goodArray);
                VoteManager.getInstance().dispatchVote(hee);
            }
            if (badArray.length > 0)
            {
                logger.debug("Bad protocol count up {0}", badArray.length);
                var boo:VoteEvent = new VoteEvent(VoteEvent.BAD_EVENT, badArray);
                VoteManager.getInstance().dispatchVote(boo);
            }
        }

        /**
         * つぶやき取得失敗時のイベントハンドラ。失敗した時点でいったん非活性化する。
         * @param tweetEvent つぶやきイベント
         */
        private function tweetFailedHandler(tweetEvent:TweetEvent):void
        {
            logger.warn("tweetFailedHandler..." + tweetEvent.info);
            if (this.dispatchedEventName == null || this.dispatchedEventName == VoteStatusEvent.ACTIVE_EVENT)
            {
                this.dispatchedEventName = VoteStatusEvent.INACTIVE_EVENT;
                var voteStatusEvent:VoteStatusEvent = new VoteStatusEvent(VoteStatusEvent.INACTIVE_EVENT, tweetEvent.info);
                VoteManager.getInstance().dispatchStatus(voteStatusEvent);
            }
            var event:VoteStatusEvent = new VoteStatusEvent(VoteStatusEvent.INACTIVE_EVENT, tweetEvent.info);
            VoteManager.getInstance().dispatchStatus(event);
        }

        /**
         * つぶやきの状態遷移イベントハンドラ
         * @param tweetEvent つぶやきイベント
         */
        private function tweetStatusHandler(tweetEvent:TweetEvent):void
        {
            logger.debug("tweetStatusHandler  info:{0}", tweetEvent.info);
        }

        //----- IDEによる自動生成 -----//
        /**
         * @private
         * stopFlagを設定する。
         */
        public function set stopFlag(value:Boolean):void
        {
            _stopFlag = value;
        }
    }
}